<!DOCTYPE html>
<html>
  <head>
    <title>
      biquad-bandpass.html
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/biquad-filters.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();

      // In the tests below, the initial values are not important, except that
      // we wanted them to be all different so that the output contains
      // different values for the first few samples.  Otherwise, the actual
      // values don't really matter.  A peaking filter is used because the
      // frequency, Q, gain, and detune parameters are used by this filter.
      //
      // Also, for the changeList option, the times and new values aren't really
      // important.  They just need to change so that we can verify that the
      // outputs from the .value setter still matches the output from the
      // corresponding setValueAtTime.
      audit.define(
          {label: 'Test 0', description: 'No dezippering for frequency'},
          (task, should) => {
            doTest(should, {
              paramName: 'frequency',
              initializer: {type: 'peaking', Q: 1, gain: 5},
              changeList:
                  [{quantum: 2, newValue: 800}, {quantum: 7, newValue: 200}],
              threshold: 3.0399e-6
            }).then(() => task.done());
          });

      audit.define(
          {label: 'Test 1', description: 'No dezippering for detune'},
          (task, should) => {
            doTest(should, {
              paramName: 'detune',
              initializer:
                  {type: 'peaking', frequency: 400, Q: 3, detune: 33, gain: 10},
              changeList:
                  [{quantum: 2, newValue: 1000}, {quantum: 5, newValue: -400}],
              threshold: 4.0532e-6
            }).then(() => task.done());
          });

      audit.define(
          {label: 'Test 2', description: 'No dezippering for Q'},
          (task, should) => {
            doTest(should, {
              paramName: 'Q',
              initializer: {type: 'peaking', Q: 5},
              changeList:
                  [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
            }).then(() => task.done());
          });

      audit.define(
          {label: 'Test 3', description: 'No dezippering for gain'},
          (task, should) => {
            doTest(should, {
              paramName: 'gain',
              initializer: {type: 'peaking', gain: 1},
              changeList:
                  [{quantum: 2, newValue: 5}, {quantum: 6, newValue: -.3}],
              threshold: 1.9074e-6
            }).then(() => task.done());
          });

      // This test compares the filter output against a JS implementation of the
      // filter.  We're only testing a change in the frequency for a lowpass
      // filter.  This assumes we don't need to test other AudioParam changes
      // with JS code because any mistakes would be exposed in the tests above.
      audit.define(
          {
            label: 'Test 4',
            description: 'No dezippering of frequency vs JS filter'
          },
          (task, should) => {
            // Channel 0 is the source, channel 1 is the filtered output.
            let context = new OfflineAudioContext(2, 2048, 16384);

            let merger = new ChannelMergerNode(
                context, {numberOfInputs: context.destination.channelCount});
            merger.connect(context.destination);

            let src = new OscillatorNode(context);
            let f = new BiquadFilterNode(context, {type: 'lowpass'});

            // Remember the initial filter parameters.
            let initialFilter = {
              type: f.type,
              frequency: f.frequency.value,
              gain: f.gain.value,
              detune: f.detune.value,
              Q: f.Q.value
            };

            src.connect(merger, 0, 0);
            src.connect(f).connect(merger, 0, 1);

            // Apply the filter change at frame |changeFrame| with a new
            // frequency value of |newValue|.
            let changeFrame = 2 * RENDER_QUANTUM_FRAMES;
            let newValue = 750;

            context.suspend(changeFrame / context.sampleRate)
                .then(() => f.frequency.value = newValue)
                .then(() => context.resume());

            src.start();

            context.startRendering()
                .then(audio => {
                  let signal = audio.getChannelData(0);
                  let actual = audio.getChannelData(1);

                  // Get initial filter coefficients and updated coefficients
                  let nyquistFreq = context.sampleRate / 2;
                  let initialCoef = createFilter(
                      initialFilter.type, initialFilter.frequency / nyquistFreq,
                      initialFilter.Q, initialFilter.gain);

                  let finalCoef = createFilter(
                      f.type, f.frequency.value / nyquistFreq, f.Q.value,
                      f.gain.value);

                  let expected = new Float32Array(signal.length);

                  // Filter the initial part of the signal.
                  expected[0] =
                      filterSample(signal[0], initialCoef, 0, 0, 0, 0);
                  expected[1] = filterSample(
                      signal[1], initialCoef, expected[0], 0, signal[0], 0);

                  for (let k = 2; k < changeFrame; ++k) {
                    expected[k] = filterSample(
                        signal[k], initialCoef, expected[k - 1],
                        expected[k - 2], signal[k - 1], signal[k - 2]);
                  }

                  // Filter the rest of the input with the new coefficients
                  for (let k = changeFrame; k < signal.length; ++k) {
                    expected[k] = filterSample(
                        signal[k], finalCoef, expected[k - 1], expected[k - 2],
                        signal[k - 1], signal[k - 2]);
                  }

                  // The JS filter should match the actual output.
                  let match =
                      should(actual, 'Output from ' + f.type + ' filter')
                          .beCloseToArray(
                              expected, {absoluteThreshold: 5.9607e-7});
                  should(match, 'Output matches JS filter results').beTrue();
                })
                .then(() => task.done());
          });

      audit.define(
          {label: 'Test 5', description: 'Test with modulation'},
          (task, should) => {
            doTest(should, {
              prefix: 'Modulation: ',
              paramName: 'frequency',
              initializer: {type: 'peaking', Q: 5, gain: 5},
              modulation: true,
              changeList:
                  [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
            }).then(() => task.done());

          });

      audit.run();

      // Run test, returning the promise from startRendering. |options|
      // specifies the parameters for the test. |options.paramName| is the name
      // of the AudioParam of the filter that is being tested.
      // |options.initializer| is the initial value to be used in constructing
      // the filter. |options.changeList| is an array consisting of dictionary
      // with two members: |quantum| is the rendering quantum at which time we
      // want to change the AudioParam value, and |newValue| is the value to be
      // used.
      function doTest(should, options) {
        let paramName = options.paramName;
        let newValue = options.newValue;
        let prefix = options.prefix || '';

        // Create offline audio context.  The sample rate should be a power of
        // two to eliminate any round-off errors in computing the time at which
        // to suspend the context for the parameter change.  The length is
        // fairly arbitrary as long as it's big enough to the changeList
        // values. There are two channels:  channel 0 is output for the filter
        // under test, and channel 1 is the output of referencef filter.
        let context = new OfflineAudioContext(2, 2048, 16384);

        let merger = new ChannelMergerNode(
            context, {numberOfInputs: context.destination.channelCount});
        merger.connect(context.destination);

        let src = new OscillatorNode(context);

        // |f0| is the filter under test that will have its AudioParam value
        // changed. |f1| is the reference filter that uses setValueAtTime to
        // update the AudioParam value.
        let f0 = new BiquadFilterNode(context, options.initializer);
        let f1 = new BiquadFilterNode(context, options.initializer);

        src.connect(f0).connect(merger, 0, 0);
        src.connect(f1).connect(merger, 0, 1);

        // Modulate the AudioParam with an input signal, if requested.
        if (options.modulation) {
          // The modulation signal is a sine wave with amplitude 1/3 the cutoff
          // frequency of the test filter.  The amplitude is fairly arbitrary,
          // but we want it to be a significant fraction of the cutoff so that
          // the cutoff varies quite a bit in the test.
          let mod =
              new OscillatorNode(context, {type: 'sawtooth', frequency: 1000});
          let modGain = new GainNode(context, {gain: f0.frequency.value / 3});
          mod.connect(modGain);
          modGain.connect(f0[paramName]);
          modGain.connect(f1[paramName]);
          mod.start();
        }
        // Output a message showing where we're starting from.
        should(f0[paramName].value, prefix + `At time 0, ${paramName}`)
            .beEqualTo(f0[paramName].value);

        // Schedule all of the desired changes from |changeList|.
        options.changeList.forEach(change => {
          let changeTime =
              change.quantum * RENDER_QUANTUM_FRAMES / context.sampleRate;
          let value = change.newValue;

          // Just output a message to show what we're doing.
          should(value, prefix + `At time ${changeTime}, ${paramName}`)
              .beEqualTo(value);

          // Update the AudioParam value of each filter using setValueAtTime or
          // the value setter.
          f1[paramName].setValueAtTime(value, changeTime);
          context.suspend(changeTime)
              .then(() => f0[paramName].value = value)
              .then(() => context.resume());
        });

        src.start();

        return context.startRendering().then(audio => {
          let actual = audio.getChannelData(0);
          let expected = audio.getChannelData(1);

          // The output from both filters MUST match exactly if dezippering has
          // been properly removed.
          let match = should(actual, `${prefix}Output from ${paramName} setter`)
                          .beCloseToArray(
                              expected, {absoluteThreshold: options.threshold});

          // Just an extra message saying that what we're comparing, to make the
          // output clearer. (Not really neceesary, but nice.)
          should(
              match,
              `${prefix}Output from ${
                                      paramName
                                    } setter matches setValueAtTime output`)
              .beTrue();
        });
      }

      // Filter one sample:
      //
      //   y[n] = b0 * x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
      //
      // where |x| is x[n], |xn1| is x[n-1], |xn2| is x[n-2], |yn1| is y[n-1],
      // and |yn2| is y[n-2].  |coef| is a dictonary of the filter coefficients
      // |b0|, |b1|, |b2|, |a1|, and |a2|.
      function filterSample(x, coef, yn1, yn2, xn1, xn2) {
        return coef.b0 * x + coef.b1 * xn1 + coef.b2 * xn2 - coef.a1 * yn1 -
            coef.a2 * yn2;
      }
    </script>
  </body>
</html>
